package aquabelva;

/**
 * WebTalkTranslator is a WebTalk to WebTalkPrime translating library.
 * @author Aqua Belva
 */


import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;
import jdsl.core.ref.NodeTree; //necessary to support the NodeTree structure
import jdsl.core.api.*;
import jdsl.core.ref.*;

public class WebTalkTranslator {
	private MetaDataLogger met;                //The associated metadata logging object
	private Map <String, Tree> defs;           //Hash map of defs
	private boolean metadataFlag = false;      //Write metadata to file?
	private String metadataFile = "";		   //Metadata will be logged to this file if flag is set
	private String nextToken="begin";		   //The next token to be parsed
	private Tree parseTree = new NodeTree();   //The Tree that is build from the input
	private int lineNumber = 0;

	/**
	 * Default constructor
	 * @author Gideon
	 */
	public WebTalkTranslator() {
		met = new MetaDataLogger ();
		defs = new HashMap<String,Tree>();
	}

	/**
	 * Constructor with metadata logging filename
	 * @param fil The file to which metadata will be logged
	 * @author Gideon
	 */
	public WebTalkTranslator(String fil) {
		this();
		metadataFlag = true;
		metadataFile = fil;
		met = new MetaDataLogger ();
	}

	/**
	 * Metadata logger accessor
	 * @return the associated metadata logging object
	 * @author Edwe
	 */
	public MetaDataLogger getMetadata() {
		return met;
	}

	/**
	 * Orchestrates the translation of a WebTalk page
	 * @param webtalk The WebTalk to be translated to WebTalkPrime
	 * @author Gideon
	 */
	public String translate (String webtalk) {
		long timeStart = System.currentTimeMillis();
		met.addSizeMetaDataFromString(webtalk); 

		String output = "";

		try {
			webtalk = resolveNests(webtalk);

			//System.out.println(
			parse(webtalk);
			preorderTraverse(parseTree.root());
			output = getWebTalkPrime();
			//output = parseTree.toString();


		}
			catch (Exception e) {
			//System.out.println(e);
			met.logError("Catastrophic failure, unable to translate page: " +e);
			parseTree = new NodeTree();
			output = "hbox(Error displaying page)"; // return this page on error

		}

		met.logMessage("Translation complete.");
		met.setTranslateTime(System.currentTimeMillis() - timeStart);

		if (metadataFlag) {
			met.toFile(metadataFile);
		}

		return output;  
	}  
	
	
	/**
	 * Orchestrates the translation of a WebTalk page
	 * @param webtalk The WebTalk to be translated to WebTalkPrime
	 * @return a Tree object with NodeElement elements as nodes. This object
	 * represents the parse tree of the input string webtalk.
	 * @author Gideon
	 */
	public Tree translateAndReturnTree (String webtalk) {
		long timeStart = System.currentTimeMillis();
		met.addSizeMetaDataFromString(webtalk); 

		String output = "";

		try {
			webtalk = resolveNests(webtalk);

			//System.out.println(
			parse(webtalk);
			preorderTraverse(parseTree.root());
			getWebTalkPrime();
			output = parseTree.toString();


		}
			catch (Exception e) {
			//System.out.println(e);
			met.logError("Catastrophic failure, unable to translate page: " +e);
			parseTree = new NodeTree();
			output = "hbox(Error displaying page)"; // return this page on error

		}

		met.logMessage("Translation complete.");
		met.setTranslateTime(System.currentTimeMillis() - timeStart);

		if (metadataFlag) {
			met.toFile(metadataFile);
		}

		return parseTree;  
	}  
	/**
	 * Method to read nested files and append their contents to the WebTalk String
	 * @author Coenie
	 * @param webtalk The webtalk String in which nests must be resolved
	 * @return Returns the webtalk string after resolving all the nests
	 */
	private String resolveNests (String webtalk) {

		StringTokenizer tok = new StringTokenizer(webtalk);

		String path = "-1";

		while(tok.hasMoreTokens()) {

			String test = tok.nextToken();
			if(test.compareToIgnoreCase("##include") == 0) {

				if(tok.hasMoreTokens()) {

					path = tok.nextToken();
				}
				else {

					return webtalk;
				}
				met.addSizeMetaData(path);

				webtalk = webtalk.replaceFirst("##include", "");

				String checkForDollar = readFromFile(path);

				checkForDollar = checkForDollar.replaceAll("\\$", ">insertDollarSignHere<");

				webtalk = webtalk.replaceFirst(path, checkForDollar);

				webtalk = webtalk.replaceAll(">insertDollarSignHere<", "\\$");

				return resolveNests(webtalk);				  
			}
		}


		return webtalk;
	}


	/**
	 * Parses a WebTalk String
	 * @param webtalk The WebTalk String which is being translated
	 * @author Dirko
	 */
	private String parse (String webtalk) {
		//parseTree.

		StringTokenizer st = new StringTokenizer(webtalk," ()#\"",true);
		nextRealToken(st);
		while(!nextToken.equals("")){

			parseSubTree(st,parseTree.root());
			

		}
		
		return "OK";

	}

	/**
	 * Parses a subtree
	 * @param st The StringTokenizer
	 * @param parent The parent node position of the subtree to be parsed
	 * @author Dirko
	 */
	private String parseSubTree(StringTokenizer st, Position parent){
		String id = parseID(st);

		if(id == null){
			met.logError("SYNTAX ERROR: ID EXPECTED", lineNumber);
			return "ERROR: ID EXPECTED";
		}


		NodeElement element = new NodeElement(id);
		Position child = parseTree.insertLastChild(parent, element);


		while(!nextToken.equals("") && nextToken.equals("#")){
			nextRealToken(st);
			parseAttr(st,element);
		}
		if(nextToken.equals("(")){
			nextRealToken(st);
			while (!nextToken.equals("")&&!nextToken.equals(")")){
				parseSubTree(st,child);
			}

			if(nextToken.equals(")")){  
				nextRealToken(st);
				return "END SUBTREE";
			}/*else if(nextToken.equals("")){
  			return "EOF";
  			}*/
			//System.out.println("   Error no end of subtree");
			met.logError("SYNTAX ERROR: ) EXPECTED", lineNumber);
			return "ERROR: NO END OF SUBTREE";// status;

		}
		return "OK";
	}

	/**
	 * Parses "attr" (refer to BNF)
	 * @param st the StringTokenizer
	 * @param element the element to which the attribute will be added
	 */
	private String parseAttr(StringTokenizer st,NodeElement element){

		String id1 = "ERROR: empty id1";
		String id2 = "ERROR: empty id2";

		id1 = parseID(st);
		if(id1 == null){
			met.logError("SYNTAX ERROR: ID EXPECTED", lineNumber);
			return "ERROR: ID EXPECTED";
		}

		if (id1.equals("")){
			met.logError("SYNTAX ERROR: ID EXPECTED", lineNumber);
			return "ERROR: NOTOKENS";
		}

		id2 = parseID(st);
		if(id1 == null){
			met.logError("SYNTAX ERROR: ID EXPECTED", lineNumber);
			return "ERROR: ID EXPECTED";
		}
		if (id1.equals("")){
			met.logError("SYNTAX ERROR: ID EXPECTED", lineNumber);
			return "ERROR: NOTOKENS";
		}

		Attribute attribute = new Attribute(id1,id2);
		element.addAttribute(attribute);

		//System.out.println("parseAttr " + id1 +" "+ id2);

		return "OK";
	}


	/**
	 * Parses "ID" (refer to BNF)
	 * @param st the StringTokenizer
	 */
	private String parseID(StringTokenizer st){
		String temp = ""; //returns "" if there are no more tokens
		if (nextToken.equals("\"")){
			temp += "\"";
			nextToken = st.nextToken();
			while (st.hasMoreTokens()&&!nextToken.equals("\"")){
				temp += nextToken;
				nextToken = st.nextToken();
			}
			temp += "\"";
			nextRealToken(st);
		}else{
			temp = nextToken;
			nextRealToken(st);
			if(temp.equals("(")||temp.equals(")")||temp.equals("#")){
				return null; //returns null if it is not a valit id
			}
		}
		return temp;
	}

	/**
	 * Helper method to remove whitespaces. Sets nextToken to the string of the next token.
	 * @param st the StringTokenizer
	 */
	private void nextRealToken(StringTokenizer st){
		if(!st.hasMoreTokens()){
			//System.out.println("No more tokens");
			nextToken =  "";
			return;
		}
		nextToken = st.nextToken();
		while(nextToken.equals(" ")||nextToken.equals("\n")||nextToken.equals("\t")){
			if(nextToken.equals("\n")){
				lineNumber ++;
			}
			if(!st.hasMoreTokens()){
				nextToken = "";
				return;
			}
			nextToken = st.nextToken();  		
		}
		//System.out.println("  df "+nextToken);
		return;
	}

	/**
	 * builds the def map and
	 * does the macrosubstitutions by
	 * preorder traversing the parse tree
	 * @param p the current position in parseTree
	 * @return the position to continue traversing
	 */
	private Position preorderTraverse(Position p){
		//pre visit
		if(p == null ){
			//ERROR
			//System.out.println("   ERROR "+p);
			met.logError("ERROR: PARSE TREE NULL ELEMENT");
			return p;
		}

		if(!parseTree.isRoot(p)){
			if(!(p.element() instanceof NodeElement)){
				//System.out.println("ERROR: node not an instance of NodeElement");
				met.logError("ERROR: PARSE TREE NODE NOT A NODELEMENT");
				return p;
			}else{
				NodeElement node = (NodeElement)p.element();

				if(node.getID().equals("def") && node.hasAttributes()){
					p=addToDefMap(p);
				}else

					/*
					 * do the macro substitution
					 */
					if(defs.containsKey(node.getID())){		

						Tree copyOfDef = copyTree(defs.get(node.getID()),defs.get(node.getID()).root());
						Position toDelete = copyOfDef.root(); //removes the root ie the "def" node

						Position temp = copyOfDef.firstChild(copyOfDef.root());

						macroSubstitute(copyOfDef.firstChild(copyOfDef.root()),copyOfDef,p);
						parseTree.replaceSubtree(p, copyOfDef);
						parseTree.contract(toDelete);

						p = temp;
					}
			}
		}

		if(parseTree.isExternal(p)){
			return p;
		}

		Position c = parseTree.firstChild(p);

		while(parseTree.lastChild(p)!=c){
			c=preorderTraverse(c);
			c = parseTree.siblingAfter(c);
		}
		preorderTraverse(c);

		return p;
	}

	/**
	 * helper method. Adds the defsubtree to the def map
	 * @param p The positoin of the def node in parseTree to add to defs
	 * @return the place in the tree to continue traversing
	 */
	private Position addToDefMap(Position p){
		if(p == null || !(p.element() instanceof NodeElement) || ((NodeElement)p.element()).getAttribute("name")==null){
			//ERROR not correct deftree
			//System.out.println("  ERROR not correct deftree");
			met.logError("ERROR: PARSE TREE NOT VALID DEFTREE");
			return p;
		}
		Position temp = p;	//position to continue traversign the tree

		Tree dst = copyTree(parseTree,p);
		String key = ((NodeElement)p.element()).getAttribute("name").getID2();
		//System.out.println(dst+"  "+key);
		defs.put(key, dst);

		return temp;
	}

	/**
	 * helper method. does the macro substitution
	 * @param p the current position in the deftree
	 * @param t the def subtree to substitute
	 * @param node the position in the parsetree to be substituted
	 */ 
	private void macroSubstitute(Position p, Tree t,Position replaceNode){

		if(!(p.element() instanceof NodeElement)|| !(replaceNode.element() instanceof NodeElement)){
			//ERROR with casting
			met.logError("ERROR: PARSE TREE CASTING ERROR");
			return;
		}
		NodeElement n = ((NodeElement)p.element());
		NodeElement replaceNodeElement = (NodeElement)replaceNode.element();

		/*
		 * replace the $attributes with the attributes in the replaceNode
		 */
		for(Map.Entry<String, Attribute> entry: n.getAttributes().entrySet()){
			String dollarAttr = entry.getValue().getID2();
			if(!dollarAttr.startsWith("$")){
				//ERROR in attribute, should start with "$"
				met.logError("ERROR: PARSE TREE ATTRIBUTE SHOULD START WITH $: "+dollarAttr);
			}else{
				String attrString = dollarAttr.substring(1, dollarAttr.length());
				if(!replaceNodeElement.containsID(attrString)){
					//ERROR the attribute to substitute was not found in the parsesubtree
					met.logError("ERROR: PARSE TREE ATTRIBUTE NOT FOUND: "+attrString);
				}else{
					String replaceWith = replaceNodeElement.getAttribute(attrString).getID2();
					Attribute newAttribute = new Attribute(entry.getValue().getID1(),replaceWith);

					entry.setValue(newAttribute);
				}
			}
		}

		/*
		 * removes the subtree in parsetree and adds to the appropriate $ reference
		 */
		if(n.getID().startsWith("$")){

			String replaceWith = n.getID().substring(1, n.getID().length());

			Position temp = t.parent(p);

			if(replaceWith.equals("*")){

				Tree children = new NodeTree();

				Position toDel = null;
				if(t.firstChild(temp)!=p){
					toDel=  t.siblingBefore(p);
				}

				PositionIterator iterator = parseTree.children(replaceNode);
				while(iterator.hasNext()){
					Position c = iterator.nextPosition();

					Tree cutTree = parseTree.cut(c);

					Position ls = children.insertLastChild(children.root(), null);
					children.replaceSubtree(ls, cutTree);
				}

				t.replaceSubtree(p, children);
				if(toDel == null){
					t.contract(t.firstChild(temp));
				}else{
					t.contract(t.siblingAfter(toDel));
				}

			}else{

				PositionIterator iterator = parseTree.children(replaceNode);
				while(iterator.hasNext()){
					Position c = iterator.nextPosition();
					if(((NodeElement)c.element()).getID().equals(replaceWith)){
						Tree cutTree = parseTree.cut(c);

						t.replaceSubtree(p, cutTree);

					}
				}
			}
			p=temp;//the place to continue
		}

		PositionIterator iterator = t.children(p);
		while(iterator.hasNext()){
			Position c = iterator.nextPosition();
			macroSubstitute(c,t,replaceNode);
		}


	}

	/**
	 * helper method. Makes a copy of t by recusively adding subtrees
	 * @param t tree to copy
	 * @param p the position currently at
	 */
	private Tree copyTree(Tree t, Position p){
		Tree temp = new NodeTree();

		temp.replaceElement(temp.root(), p.element());

		if(t.isExternal(p)){
			return temp;
		}

		Position c=null;
		PositionIterator iter = t.children(p);
		while(iter.hasNext()){
			c = iter.nextPosition();
			Position newChild = temp.insertLastChild(temp.root(), null); 
			temp.link(newChild, copyTree(t,c));
		}
		return temp;
	}

	/**
	 * returns the webtalk prime string after the parsetree has been built and
	 * substituted. 
	 * @return a string in the webtalkprime language
	 */
	private String getWebTalkPrime(){
		return preorderWebTalkPrime(parseTree.root());
	}

	/**
	 * helper to getWebTalkPrime. preorder traverse the parsetree, ignoring all
	 * "def" nodes.
	 */
	private String preorderWebTalkPrime(Position p){
		String temp = "";

		if (p==null){
			//ERROR
			met.logError("ERROR: WEBTALK PRIME NULL PARSETREE ELEMENT");
			return "";
		}
		if(parseTree.isRoot(p)){
			//ignores the root
		}else{
			if (!(p.element() instanceof NodeElement)){
				//ERROR
				met.logError("ERROR: WEBTALK PRIME TREE NOT NODEELEMENT");
				return"";
			}

			NodeElement node = (NodeElement)p.element();

			if(node.getID().equals("def") && node.hasAttributes()){
				//ignores defs
				return "";
			}

			temp += node.toString() + " ";
		}

		if(parseTree.isExternal(p)){
			return temp;
		}

		if(!parseTree.isRoot(p)){
			temp += "( ";
		}

		PositionIterator iterator = parseTree.children(p);
		while(iterator.hasNext()){
			Position c = iterator.nextPosition();
			temp += preorderWebTalkPrime(c);
		}

		if(!parseTree.isRoot(p)){
			temp += ") ";
		}

		return temp;
	}


	/**
	 * Method to read from files (platform independent) 
	 * @author Coenie
	 * @param filename 
	 * @return String containing the contents of the file specified
	 */
	private String readFromFile (String filename) {
		File file = new File(filename) ; 

		String content = "";

		BufferedReader read = null;

		try {

			read = new BufferedReader(new FileReader(file));

		}
		catch(FileNotFoundException e) {
			met.logError("File not found, caught: "+e);
			return "";
		}

		try {

			String line = read.readLine();

			content += line;

			while((line = read.readLine()) != null) {

				content += "\n" + line;
			}
		}
		catch(IOException e) {
			met.logError("Error reading from file, caught: "+e);
			return "";
		}

		return content;
	}


	/**
	 * Testing main
	 */
	public static void main(String args[]){
		
	  /*
	   * MetaDataLogger & WebTalkError tests (Edwe)
	   * Note that you require two test files, LoggerTest1.txt and LoggerTest2.txt
	   * in the current directory. These files can contain absolutely anything.
	   * (Select the code below and press ctrl+shift+c to uncomment)
	   */
	  
//		WebTalkTranslator p = new WebTalkTranslator("log.txt");
		
		
//	  MetaDataLogger testLogger = new MetaDataLogger();
//	  
//	  /*Test automatic logging from valid file*/
//	  System.out.print("Test 1 (automatic logging from file 1):");
//	  
//	  boolean success = testLogger.addSizeMetaData ("LoggerTest1.txt"); 
//	  
//   if (success) {
//    System.out.println("PASS\n" + testLogger.toString() + "\n");
//	  }
//   else System.out.println ("FAIL\n");
//	  
//	  System.out.print("Test 2 (automatic logging from file 2):");
//	  success = testLogger.addSizeMetaData ("LoggerTest2.txt"); 
//	  
//	  if (success) {
//	    System.out.println("PASS\n" + testLogger.toString() + "\n");
//	  }
//	  else System.out.println ("FAIL\n");
//	  
//	  /*Test attempted automatic logging from invalid file*/
//	  System.out.print("Test 3 (attempted automatic logging from invalid file):");
//	  success = testLogger.addSizeMetaData ("<test invalid file name>"); 
//	  
//	  if (success) {
//	    System.out.println("FAIL\n" + testLogger.toString() + "\n");
//	  }
//	  else System.out.println ("PASS (Exception handled)\n");
//	  
//	  /*Test automatic logging from String*/
//	  System.out.print("Test 4 (automatic logging from String):");
//	  String testStr = "line1\nline2\nline3\nline4";
//	  
//	  success = testLogger.addSizeMetaDataFromString (testStr); 
//	  
//	  if (success) {
//	    System.out.println("PASS\n" + testLogger.toString() + "\n");
//	  }
//	  else System.out.println ("FAIL");
//	  
//	  /*Test reset*/
//	  System.out.print("Test 5 metadata logger reset:");
//	  testLogger.reset();
//	  System.out.println("PASS (if this line precedes a blank line)\n" + testLogger.toString() + "\n");
//	
//	  /*Test manual parameter sets*/
//	  System.out.print("Test 6 manual metadata sets:");
//	  testLogger.setLineCount(100);
//	  testLogger.setFileSize(100);
//	  testLogger.setTranslateTime(100);
//	  System.out.println("PASS (if line count, file size and translate time all equal 100)\n" + testLogger.toString() + "\n");
//		
//	  /*Test error logging without line numbers*/
//    System.out.print("Test 7 error logging (no line numbers):");
//	  testLogger.logError("This is an error without a line number");
//	  System.out.println("PASS (verify manually)\n" + testLogger.toString() + "\n");
//	
//	  /*Test error logging with line numbers*/
//	  System.out.print("Test 8 error logging (including line number):");
//	  testLogger.logError("This is an error with a line number '77'", 77);
//	  System.out.println("PASS (verify manually)\n" + testLogger.toString() + "\n");
//	
//	  /*Test logging to invalid file*/
//	  System.out.print("Test 9 logging to invalid file:");
//	  success = testLogger.toFile("/ggg/log.txt");
//	  
//	  if (success) {
//		    System.out.println("FAIL (MetaDataLogger thinks the write was successful)\n");
//		  }
//		  else System.out.println ("PASS (Exception handled)\n");
//	  
//  /*Test logging to valid file*/
//	  System.out.print("Test 10 logging to valid file:");
//	  success = testLogger.toFile("log.txt");
//	  
//	  if (success) {
//		  System.out.println("PASS (verify manually that log.txt in the current directory)\n"+ 
//				 "contains the following:\n" + testLogger.toString() + "\n");
//		  }
//		  else System.out.println ("FAIL");
	  
	  /*Test message logging*/
//	  System.out.print("Test 11 message logging:");
//	  testLogger.logMessage("This is some message");
//	  testLogger.logMessage("This is another message");
//	  System.out.println("PASS (verify manually that two messages have been logged)\n"
//			  + testLogger.toString() + "\n");
//	  
//	  System.out.println ("--THIS CONCLUDES THE TESTING OF MetaDataLogger.java and WebTalkError.java--");
   
		
		/*
		 * END MetaDataLogger & WebTalkError tests (Edwe)
		 */
		
		
//		WebTalkTranslator p = new WebTalkTranslator("log.txt");
//		//System.out.println(p.readFromFile("file.txt")); //test for correct file -- Coenie
//		//System.out.println(p.readFromFile("file2.txt")); //test for no file --Coenie
//		System.out.println(p.translate("def #name page( vbox #title $title ( $*)) " +
//		"def #name par( hfbox ( $*)) " +
//		"page #title Greet (par ( \"END SUBTREE\" " +
//		"sdf \"sdfsfaf\" sfe'fe'f)) "));
//
////		System.out.println(p.parseTree.toString());
////		System.out.println("---------------------");
////		p.preorderTraverse(p.parseTree.root());
////		System.out.println("---------------------");
////		System.out.println(p.parseTree.toString());
////		System.out.println("---------------------");
////		System.out.println(p.getWebTalkPrime());
//
//
//		//System.out.println(p.translate(p.readFromFile("inc1.txt")));
//		System.out.println(p.resolveNests("include inc1.txt"));
//
//		//p.getMetadata().toFile("log.txt"); //tests Edwe's getMetadata() and toFile() methods as well as Coenie's readFromFile() method -- Coenie
	}

}
